package com.example.emos.wx.config.shrio;

import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.Struct;
import java.util.concurrent.TimeUnit;

@Component
/**
 * 加上这个注解之后，这个java类就是多例的java类了
 */
@Scope("prototype")
public class OAuth2Filter extends AuthenticatingFilter {

    /**
     * 忘里面保存数据
     */
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 拦截请求后，用于把令牌字符串封装成令牌对象
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = getRequestToken(req);
        if (StrUtil.isBlank(token)){
            return null;
        }
        //如果不为空，封装为令牌对象
        return new OAuth2Token(token);
    }

    /**
     * 拦截请求，判断请求是否需要被 Shiro 处理
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req = (HttpServletRequest) request;
        //判断这一次请求是否是options类型的
        if (req.getMethod().equals(RequestMethod.OPTIONS.name())){
            //如果是  就放行
            return true;
        }
        //不是的话  不能放行，必须交给Shiro框架处理
        return false;
    }

    /**
     * 该方法用于处理所有被 Shiro 处理的请求
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        //设置响应的字符集
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");

        //往响应头里面设置跨域的参数(允许跨域请求)
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));

        threadLocalToken.clear();

        String token = getRequestToken(req);
        if (StrUtil.isBlank(token)){
            //如果为空，直接给客户端返回一个错误消息
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效的令牌");
            //只有令牌没有问题的时候才会去执行reaml类
            return false;
        }
        try {
            //验证令牌是否有效
            jwtUtil.verifierToken(token);
        }catch (TokenExpiredException e){
            //判断redis里面是否还有 token 这个数据
            if (redisTemplate.hasKey(token)){
                //如果有，说明只是客户端的令牌过期了，服务端的令牌还没过期，所以我们需要生成一个新的令牌
                redisTemplate.delete(token);
                int userId = jwtUtil.getUserId(token);
                //根据老令牌获取到的userId新生成一个令牌token
                token = jwtUtil.createToken(userId);
                //将令牌重新存入redis
                redisTemplate.opsForValue().set(token, userId+"", cacheExpire, TimeUnit.DAYS);
                threadLocalToken.setToken(token);
            }
            else {
                //如果客户端令牌过期了，服务端的令牌也过期了，那就需要用户重新登录
                resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
                resp.getWriter().print("令牌已到期，请重新登录");
                return false;
            }

            //这里捕获的是token内容异常
        }catch (Exception e){
            resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
            resp.getWriter().print("无效令牌");
            return false;
        }

        //没有问题  执行 reaml类, 但只能间接调用
        //这个方法会让Shiro间接的去执行reaml类
        boolean bool = executeLogin(request, response);

        //如果这里返回的是false，那就是认证和授权失败
        return bool;
    }

    /**
     * Shiro判断 用户没有登录或者 登录失败 就会执行这个方法 （肯定认证失败的时候会触发执行）
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        //设置响应的字符集
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");

        //往响应头里面设置跨域的参数(允许跨域请求)
        resp.setHeader("Access-Control-Allow-Credentials", "true");
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));

        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
        try {
            resp.getWriter().print(e.getMessage());
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }

        return false;
    }

    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 从请求头或者请求体中获取token
     * @param request
     * @return
     */
    private String getRequestToken(HttpServletRequest request){
        String token =  request.getHeader("token");
        if (StrUtil.isBlank(token)){
            token = request.getParameter("token");
        }
        return token;
    }
}
